package com.wongsi.spring5.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;
import javax.sql.DataSource;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * HttpSecurity对象，能够用来配置在Web级别该如何处理安全性。
     *
     * 用来定义如何保护路径的配置方法：
     *
     * access(String) 如果给定的SpEL表达式计算结果为true，就允许访问
     * anonymous() 允许匿名用户访问
     * authenticated() 允许认证过的用户访问
     * denyAll() 无条件拒绝所有访问
     * fullyAuthenticated() 如果用户是完整认证（not Remember-me）,就允许访问
     * hasAnyAuthority(String...) 如果用户具备给定权限中的某一个，就允许访问
     * hasAnyRole(String...) 如果用户具备给定角色中的某一个，就允许访问
     * hasAuthority(String) 如果用户具备给定权限，就允许访问
     * hasIpAddress(String) 如果请求来自给定IP地址，就允许访问
     * hasRole(String) 如果用户具备给定角色，就允许访问
     * not() 对其他访问方法的结果求反
     * permitAll() 无条件允许访问
     * rememberMe() 如果用户是通过Remember-me功能认证的，就允许访问
     *
     * Spring Security对Spring表达式语言的扩展：
     *
     * authentication 用户的认证对象
     * denyAll 结果始终为false
     * hasAnyRole(list of roles) 如果用户被授予了列表中任意的指定角色，结果为true
     * hasRole(role) 如果用户被授予了指定的角色，结果为true
     * hasIpAddress(IP Address) 如果请求来自指定IP，结果为true
     * isAnonymous() 如果当前用户为匿名用户，结果为true
     * isAuthenticated() 如果当前用户进行了认证，结果为true
     * isFullyAuthenticated() 如果当前用户进行了完整认证（不是通过Remember-me功能进行的认证），结果为true
     * isRememberMe() 如果当前用户是通过Remember-me自动认证的，结果为true
     * permitAll 结果始终为true
     * principal 用户的principal对象
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            //////////// 授权相关的配置
            .authorizeRequests()
            .antMatchers("/design", "/orders")
            .hasRole("USER")
            // 这些规则的顺序是很重要的。
            // 声明在前面的安全规则比后面声明的规则有更高的优先级。
            // 如果我们交换这两个安全规则的顺序，
            // 那么所有的请求都会有permitAll()的规则，
            // 对“/design”和“/orders”声明的规则就不会生效了。
            .antMatchers("/", "/**").permitAll()
            //////////// 授权相关的配置 end

            // 在开始新的配置区域时，我们可以多次调用and()。
            .and()
            .formLogin()
            .loginPage("/login")
            // 登录成功，强制跳转到Taco设计页
            .defaultSuccessUrl("/design", true)

            // 退出
            .and()
            .logout()
            .logoutSuccessUrl("/")
        ;
    }

    @Resource
    private UserDetailsService userDetailsService;

    // 对于encoder()的任何调用都会被拦截，
    // 并且会返回应用上下文中的bean实例。
    @Bean
    public PasswordEncoder encoder() {
        return new BCryptPasswordEncoder(4);
    }

    /**
     * <h2>自定义的用户详情服务</h2>
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(encoder());
    }

    @Resource
    DataSource dataSource;

    /**
     * <h2>重写默认的用户查询功能</h2>
     *
     * Spring Security内部查找用户信息时所执行的SQL查询语句
     *
     * <pre>
     * public static final String DEF_USERS_BY_USERNAME_QUERY =
     *         "select username,password,enabled " +
     *             "from users " +
     *             "where username = ?";
     * public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
     *         "select username,authority " +
     *             "from authorities " +
     *             "where username = ?";
     * public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY =
     *         "select g.id, g.group_name, ga.authority " +
     *             "from groups g, group_members gm, group_authorities ga " +
     *             "where gm.username = ? " +
     *             "and g.id = ga.group_id " +
     *             "and g.id = gm.group_id";
     * </pre>
     *
     * 可能你的数据库与上述的不一致，那么我们可以配置自己的查询：
     *
     * 本方法重写了认证和基本权限的查询语句
     *
     * 通过调用groupAuthorities ByUsername()方法，
     * 我们也能够将群组权限重写为自定义的查询语句。
     *
     * <h2>使用转码后的密码</h2>
     *
     * 内置的实现:
     * <ol>
     * <li>BCryptPasswordEncoder：使用bcrypt强哈希加密。</li>
     * <li>NoOpPasswordEncoder：不进行任何转码。</li>
     * <li>Pbkdf2PasswordEncoder：使用PBKDF2加密。</li>
     * <li>SCryptPasswordEncoder：使用scrypt哈希加密。</li>
     * <li>StandardPasswordEncoder：使用SHA-256哈希加密。</li>
     * </ol>
     *
     * 可以提供自定义的实现 {@link org.springframework.security.crypto.password.PasswordEncoder}
     *
     * 不管你使用哪一个密码转码器，都需要理解一点，即数据库中的密码是永远不会解码的。
     * 用户在登录时所采取的策略与之相反，
     * 输入的密码会按照相同的算法进行转码，
     * 然后与数据库中已经转码过的密码进行对比。
     * 这个对比是在PasswordEncoder的matches()方法中进行的。
     *
     * @author wongsi
     * @since 4.2

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        /*
         * 将默认的SQL查询替换为自定义的设计时，很重要的一点就是要遵循查询的基本协议。
         *
         * 所有查询都将用户名作为唯一的参数。
         * 认证查询会选取用户名、密码以及启用状态信息。
         * 权限查询会选取零行或多行包含该用户名及其权限信息的数据。
         * 群组权限查询会选取零行或多行数据，每行数据中都会包含群组ID、群组名称以及权限。

        auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery(
            "select username, password, enabled from Users where username=?")
            .authoritiesByUsernameQuery(
                "select username, authority from UserAuthorities " +
                    "where username=?")
            .passwordEncoder(new BCryptPasswordEncoder(4));
    }*/

    /*
     * <h2>使用基于LDAP 认证</h2>
     *
     * LDAP（Lightweight Directory Access Protocol，轻量级目录访问协议）
     * @since 4.3

    @Override
    protected void configure(AuthenticationManagerBuilder auth)
        throws Exception {
        auth
            .ldapAuthentication()
            // 方法userSearchFilter()和groupSearchFilter()用来为基础LDAP查询提供过滤条件，
            // 它们分别用于搜索用户和组。
            // 默认情况下，对于用户和组的基础查询都是空的，也就是表明搜索会在LDAP层级结构的根开始。
            //
            // 但是我们可以通过指定查询基础来改变这个默认行为：
            .userSearchBase("ou=people")
            .userSearchFilter("(uid={0})")
            .groupSearchBase("ou=groups")
            .groupSearchFilter("member={0}")
            // 密码比对
            .passwordCompare()
            // 密码使用bcrypt密码哈希函数加密。这需要LDAP服务器上的密码也使用bcrypt进行了加密。
            .passwordEncoder(new BCryptPasswordEncoder())
            // 指定了要与给定密码进行比对的是“passcode”属性
            .passwordAttribute("passcode")
            // 默认情况下，Spring Security的LDAP认证假设LDAP服务器监听本机的33389端口。
            // 但是，如果你的LDAP服务器在另一台机器上，那么可以使用contextSource()方法来配置这个地址：
//            .contextSource()
//            .url("ldap://tacocloud.com:389/dc=tacocloud,dc=com")
            // ...
        ;
    }*/

    /*
     * 内存方式
     * @since 4.1
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("buzz")
            .password("infinity")
            .authorities("ROLE_USER")
            .and()
            .withUser("woody")
            .password("wong")
            .authorities("ROLE_USER");
    }
    */
}
